/*
 * Copyright 2011-2012 Marcin Blazejewski
 *
 * This file is part of Ground0.
 *
 * Ground0 is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Ground0 is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Ground0. If not, see <http://www.gnu.org/licenses/>.
 *
 */

#include <config.h>

#include "geometry.hpp"
#include "stuff.hpp"
#include "wloader.hpp"
#include "collision.hpp"

#include <iostream>
#include <fstream>
#include <sstream>
#include <math.h>
#include <vector>
#include <map>
#include <string>
#include <string.h>

using namespace std;


static int initialized = 0;
static map<string, Material*> matbase;
static map<string, Block*>    objbase;
static map<string, CollisionModel*>  cmbase;

string basename(const string &s) {
    size_t ls = s.find_last_of('/');
    if (ls != string::npos)  return s.substr(0, ls);
    else return string();
}


ObjFile::ObjFile(const char *fname) 
    : f(fname, fstream::in), source_file(fname) {
    if (!f.good()) {
        throw string("object file not open: ") + string(fname);
    }

}

ObjFile::~ObjFile() {
    for (int i = 0; i < vertices.size(); i++)
        delete vertices[i];

    for (int i = 0; i < normals.size(); i++)
        delete normals[i];

    for (int i = 0; i < tex_coords.size(); i++)
        delete tex_coords[i];

}

void ObjFile::parse() {
    string line;
    string cmd;
    string objname("default");
    string material("default");
    // empty face list to be used as a template when adding a new face set
    vector<Face> face_init; 

    vertices.clear();
    tex_coords.clear();
    normals.clear();
    faces.clear();
    vector<Face> *current_face_set = NULL;

    while (f.good()) {
        getline(f, line);
        if (line.size() < 1) continue;
        
        stringstream ss(line);
        ss >> cmd;

        // object
        if (cmd[0] == '#') {
            // ignore
        }
        else if (cmd.compare("o") == 0) {
            ss >> objname;
        }
        // set active material
        else if (cmd.compare("usemtl") == 0) {
            ss >> material;

            // add a new material and a corresponding face set
            matnames.push_back(material);
            faces.push_back(face_init);
            current_face_set = &faces[faces.size() - 1];
        }
        // vertex
        else if (cmd.compare("v") == 0) {
            float x, y, z;
            ss >> x >> y >> z;
            
            vertices.push_back(new Vector(x, y, z));
        }
        // vertex normal
        else if (cmd.compare("vn") == 0) {
            float x, y, z;
            ss >> x >> y >> z;
            normals.push_back(new Vector(x, y, z));
        }
        // vertex texture coordinates
        else if (cmd.compare("vt") == 0) {
            float x, y, z;
            ss >> x >> y >> z;
            // convert v-coord:
            // (0,0) in obj file tex coords is top-left
            // (0,0) in opengl tex coords is bottom left
            y = 1.0f - y;

            tex_coords.push_back(new Vector(x, y, z));
        }

        // face
        else if (cmd.compare("f") == 0) {
            int v[4];
            int vt[4];
            int vn[4];
            memset(v, 0, sizeof(v));
            memset(vn, 0, sizeof(vn));
            memset(vt, 0, sizeof(vt));

            for (int i = 0; i < 4; i++) {
                
                // read vertex index
                ss >> v[i];

                // optional: read texture coord index
                if (ss.peek() == '/') {
                    ss.ignore(1);
                    if (ss.peek() != '/') {
                        ss >> vt[i];
                    }
                }
                
                // optional: read normal index
                if (ss.peek() == '/') {
                    ss.ignore(1);
                    if (isdigit(ss.peek())) {
                        ss >> vn[i];
                    }
                }

            }

            // create new Face object
            // TODO: validate vertex indexes
            Face face(vertices[v[0]-1], vertices[v[1]-1], vertices[v[2]-1]);

            // fill in normal vector index, if given and valid
            for (int i = 0; i < 3; i++) {
                // obj file counts vectors from 1, not zero
                int normal_idx = vn[i] - 1;
                // validate: see if we have this in our normal vectors array
                if ( (normal_idx < normals.size()) &&
                     (normal_idx >= 0) ) {
                    face.normals[i] = normals[normal_idx];
                }
            }
            
            // fill in tex coord vector index, if given and valid
            for (int i = 0; i < 3; i++) {
                int tex_idx = vt[i];
                if ( (tex_idx >=0 ) && (tex_idx < tex_coords.size()) ) {
                    face.texcoords[i] = tex_coords[tex_idx];
                }
            }
            
            if (current_face_set == NULL) {
                cout << "[warn  ] .. no material specified, using default" << endl;
                faces.push_back(face_init);
                current_face_set = &faces[faces.size() - 1];
                matnames.push_back(material);
            }
            current_face_set->push_back(face);
        }

        // mtlib
        else if (cmd.compare("mtllib") == 0) {
            string msfile;
            ss >> msfile;
            matsource = basename(source_file) + "/" + msfile;
        }

        else {
            cout << "[warn  ] .. unsupported statement: " << line << endl;
        }

    }

    f.close();
}

void ObjFile::extract(map<string, Block*> &objbase, 
                      map<string, Material*> &matbase,
                      const char *set_name) {

    vector<int>::iterator fi;
    map<string, Material*>::iterator mi;
    string name(set_name);
    BlockBuilder bb;

    // load materials

    if (matsource.size() > 0) {
        cout << "[info  ] .. loading associated material library " << matsource << endl;
        MatFile mf(matsource.c_str());
        mf.parse();
        mf.copy_matlib(matbase);
    }

    // add vertices, normals and texcoords

    for (int i = 0; i < vertices.size(); i++)
        bb.add_vertex(*vertices[i]);

    for (int i = 0; i < normals.size(); i++)
        bb.add_vertex(*normals[i]);

    for (int i = 0; i < tex_coords.size(); i++)
        bb.add_vertex(*tex_coords[i]);

    // add faces

    for (int i = 0; i < faces.size(); i++) {
        string mn = matnames[i];
        mi = matbase.find(mn);

        if (mi == matbase.end()) {
            cout << "[warn  ] .. material '" << mn << "' not found, using default" << endl;
            mi = matbase.find("default");
        }

        if (mi == matbase.end()) {
            throw string("no default material in matbase");
        }

        int fs = bb.add_face_set( mi->second );
        //b->materials.push_back( mi->second );

        for (int j = 0; j < faces[i].size(); j++) {
            bb.add_face(fs, faces[i][j]);
        }
    }

    Block *b = bb.get_block();
    b->calc_normals();
    objbase[name] = b;
}


MatFile::MatFile(const char *fname) :
    source_file(fname), f(fname, fstream::in) {

    if (!f.good()) {
        throw string("material file not open: ") + string(fname);
    }
}

void MatFile::parse() {
    string line;
    string cmd;
    string matname;  // active material
    Material *m = NULL;

    while (f.good()) {
        getline(f, line);
        if (line.size() < 1) continue;
        
        stringstream ss(line);
        ss >> cmd;
        
        // new material
        if (cmd[0] == '#') {
            // ignore
        }
        else if (cmd.compare("newmtl") == 0) {
            if (m) {
                matlib[matname] = m;
            }
            ss >> matname;
            m = new Material();
        }

        // ambient color
        else if (cmd.compare("Ka") == 0) {
            if (!m) cerr << "[error ] .... no active material, ignoring line" << endl;
            float r,g,b;
            ss >> r >> g >> b;
            m->ambient[0] = r;
            m->ambient[1] = g;
            m->ambient[2] = b;
            m->ambient[3] = 1.0;
        }

        // diffuse color
        else if (cmd.compare("Kd") == 0) {
            if (!m) cerr << "[error ] .... no active material, ignoring line" << endl;
            float r,g,b;
            ss >> r >> g >> b;
            m->diffuse[0] = r;
            m->diffuse[1] = g;
            m->diffuse[2] = b;
            m->diffuse[3] = 1.0;
        }

        // specular
        else if (cmd.compare("Ks") == 0) {
            if (!m) cerr << "[error ] .... no active material, ignoring line" << endl;
            float r,g,b;
            ss >> r >> g >> b;
            m->specular[0] = r;
            m->specular[1] = g;
            m->specular[2] = b;
            m->specular[3] = 1.0;
        }

        // texture - diffuse
        else if (cmd.compare("map_Kd") == 0) {
            if (!m) cerr << "[error ] .... no active material, ignoring line" << endl;
            string texfile, texpath;
            ss >> texfile;
            texpath = basename(source_file) + "/" + texfile;

            m->set_texture(texpath.c_str());
        }
        
        // unknown
        else {
            //cout << "[warn  ] .... unsupported statement: " << line << endl;
        }

        if (m) {
            matlib[matname] = m;
        }
    }
}


void MatFile::copy_matlib(map<string, Material*> &copy_to) {
    map<string, Material*>::iterator mi;
    
    for (mi = matlib.begin(); mi != matlib.end(); mi++) {
        copy_to[mi->first] = mi->second;
    }
}


/******************************************************************************/

void load_model(const char *file, const char *name) {
    cout << "[info  ] load model " << file << " as " << name << endl;

    if (initialized == 0) {
        matbase["default"] = new Material(0.6, 0.6, 0.6);
        initialized = 1;
    }

    ObjFile src(file);
    src.parse();
    src.extract(objbase, matbase, name);

    string cm_file(file);
    size_t ext_pos = cm_file.find_last_of('.');
    if (ext_pos != string::npos) cm_file = cm_file.substr(0, ext_pos);
    cm_file += ".cm";
    CollisionModel *cm = new CollisionModel();
    string _name(name);
    try {
        ifstream ifs(cm_file.c_str(), ios_base::binary);
        
        cm->load(ifs);
        ifs.close();

        cout << "[debug ] .. loaded associated CM: " << cm_file << endl;
    }
    catch (...) {
        cout << "[debug ] .. using default empty CM for " << name << endl;
    }
    cmbase[_name] = cm;
}


Block* get_model(const char *name) {
    map<string, Block*>::iterator i = objbase.find(name);
    if (i == objbase.end()) throw string("no such model in objbase: ") + string(name);
    if (i->second == NULL) throw string("corrupt model in objbase: ") + string(name);

    return i->second;
}

CollisionModel* get_cm(const char *name) {
    map<string, CollisionModel*>::iterator i = cmbase.find(name);
    if (i == cmbase.end()) return NULL;
    return i->second;
}

void wloader_destroy() {
    map<string, Material*>::iterator i;
    map<string, Block*>::iterator j;

    for (i=matbase.begin(); i!=matbase.end(); i++) {
        delete i->second;
    }
    for (j=objbase.begin(); j!=objbase.end(); j++) {
        delete j->second;
    }
}


/* EOF */
